package com.fowo.api.sys.cns.aop;

import com.fowo.api.common.annotaion.DataObjectType;
import com.fowo.api.common.util.ContextHolder;
import com.fowo.api.sys.cns.ChangeNotice;
import com.fowo.api.sys.cns.ChangeNoticeListener;
import com.fowo.api.sys.cns.ChangeNoticeService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.*;

/**
 * 对象服务方法自动 AOP 接入变更通知
 * @author yl_ls
 */
@Component
@Aspect
public class ObjectServiceMethodChangeAspect {

    @Resource
    private ChangeNoticeService changeNoticeService;

    /**
     * 对象创建方法切面
     */
    @Pointcut("execution(Long com.fowo.api..service..create(..))")
    public void createCutPoint() {}

    /**
     * 对象更新方法切面
     */
    @Pointcut("execution(* com.fowo.api..service..update*(..))")
    public void updateCutPoint() {}

    /**
     * 对象更新方法切面
     */
    @Pointcut("execution(* com.fowo.api..service..delete*(..))")
    public void deleteCutPoint() {}

    @AfterReturning(value = "createCutPoint()", returning = "id")
    public void afterCreate(JoinPoint joinPoint, Long id) {
        String objectName = solveObjectName(joinPoint);
        if (objectName != null && id != null) {
            changeNoticeService.noticeInserts(objectName, id);
        }
    }

    @Around("updateCutPoint()")
    public Object aroundUpdate(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object result = proceedingJoinPoint.proceed();
        String objectName = solveObjectName(proceedingJoinPoint);
        if (objectName != null) {
            Object[] args = proceedingJoinPoint.getArgs();
            if (args.length > 0) {
                Object arg = args[0];
                List<Long> keys = solveIds(arg, true);
                if (keys != null) {
                    changeNoticeService.noticeUpdates(objectName, keys);
                } else {
                    changeNoticeService.noticeBatchUpdate(objectName);
                }
            }
        }
        return result;
    }

    @After("deleteCutPoint()")
    public void afterDelete(JoinPoint joinPoint) {
        String objectName = solveObjectName(joinPoint);
        if (objectName != null) {
            Object[] args = joinPoint.getArgs();
            if (args.length > 0) {
                Object arg = args[0];
                List<Long> keys = solveIds(arg, true);
                if (keys != null) {
                    changeNoticeService.noticeRemoves(objectName, keys);
                } else {
                    changeNoticeService.noticeBatchUpdate(objectName);
                }
            }
        }
    }

    @Around("@annotation(changeNotice)")
    public Object aroundAnnotation(ProceedingJoinPoint proceedingJoinPoint, ChangeNotice changeNotice) throws Throwable {
        Object o = proceedingJoinPoint.proceed();
        String objectName = changeNotice.objectName();
        if (StringUtils.hasText(objectName)) {
            List<Long> keys = (changeNotice.keyAccessor().length > 0) ? runKeyAccessor(changeNotice.keyAccessor()[0], proceedingJoinPoint.getArgs(), o) : null;
            if (changeNotice.changeType() == ChangeNotice.ChangeType.Insert) {
                if (keys == null) {
                    keys = solveIds(o, true);
                }
            } else {
                if (keys == null) {
                    keys = solveIds(proceedingJoinPoint.getArgs()[0], true);
                }
            }
            if (keys != null && !keys.isEmpty()) {
                if (changeNotice.changeType() == ChangeNotice.ChangeType.Insert) {
                    changeNoticeService.noticeInserts(objectName, keys);
                }
                else if (changeNotice.changeType() == ChangeNotice.ChangeType.Update) {
                    changeNoticeService.noticeUpdates(objectName, keys);
                }
                else {
                    changeNoticeService.noticeRemoves(objectName, keys);
                }
            }
        }
        return o;
    }

    private static final ThreadLocal<Map<Class<? extends ChangeNoticeListener.ChangeNoticeKeyAccessor>, ChangeNoticeListener.ChangeNoticeKeyAccessor>> KEY_ACCESSOR_CACHE = new ThreadLocal<>();
    private List<Long> runKeyAccessor(Class<? extends ChangeNoticeListener.ChangeNoticeKeyAccessor> keyAccessorClass, Object[] args, Object returnValue) {
        Map<Class<? extends ChangeNoticeListener.ChangeNoticeKeyAccessor>, ChangeNoticeListener.ChangeNoticeKeyAccessor> cache = KEY_ACCESSOR_CACHE.get();
        if (cache == null) {
            cache = new HashMap<>();
            KEY_ACCESSOR_CACHE.set(cache);
        }
        if (!cache.containsKey(keyAccessorClass)) {
            cache.put(
                    keyAccessorClass,
                    ContextHolder.getApplicationContext().getBean(keyAccessorClass)
            );
        }
        ChangeNoticeListener.ChangeNoticeKeyAccessor keyAccessor = cache.get(keyAccessorClass);
        return keyAccessor.getChangeKeys(args, returnValue);
    }

    private List<Long> solveIds(Object arg, boolean solveList) {
        // 更新方法第一个参数，
        // 要不直接是 Long 的更新主键，
        // 要不是包括 id 的更新模型，或以上方式的列表
        if (solveList && arg instanceof List) {
            List<Long> list = new ArrayList<>(((List<?>) arg).size());
            for (Object o : ((List<?>) arg)) {
                List<Long> childIds = solveIds(o, false);
                if (childIds != null && !childIds.isEmpty()) {
                    list.addAll(childIds);
                }
            }
            return list;
        }
        else if (arg instanceof Long) {
            return Collections.singletonList((Long) arg);
        }
        else {
            Field idField = ReflectionUtils.findField(arg.getClass(), "id", Long.class);
            if (idField != null) {
                idField.setAccessible(true);
                Long id = null;
                try {
                    id = (Long) idField.get(arg);
                } catch (IllegalAccessException e) {
                    return null;
                }
                if (id != null) {
                    return Collections.singletonList(id);
                }
                idField.setAccessible(false);
            }
        }
        return null;
    }

    private String solveObjectName(JoinPoint joinPoint) {
        DataObjectType dataObjectType = joinPoint.getThis().getClass().getAnnotation(DataObjectType.class);
        if (dataObjectType != null) {
            return dataObjectType.value().getSimpleName();
        }
        return null;
    }
}
